package com.bytebuddytest.fieldandmethod;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.*;
import net.bytebuddy.matcher.ElementMatchers;
import org.junit.Assert;
import org.junit.Test;

import java.io.File;
import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Function;

import static net.bytebuddy.matcher.ElementMatchers.named;

/**
 * test method delegation
 * <p>
 * test relevant annotations
 *
 * @author yutianhong
 * @version 1.0
 * @since 2023/09/26 14:24
 */
@SuppressWarnings({"unused", "resource"})
public class MethodDelegationTest {
    public static class Source {
        public String hello(String name) {
            return null;
        }
    }

    public static class Target {
        public static String intercept(String s) {
            return "String intercept(String s)";
        }

        public static String intercept(int i) {
            return "String intercept(int i)";
        }

        public static String intercept(Object o) {
            return "String intercept(Object o)";
        }
    }

    @Test
    public void testMethodDelegationDecision() throws Exception {
        String helloWorld = new ByteBuddy()
                .subclass(Source.class)
                .method(named("hello")).intercept(MethodDelegation.to(Target.class))
                .make()
                .load(getClass().getClassLoader())
                .getLoaded()
                .newInstance()
                .hello("World");
        // `String intercept(int i)` can't fit
        // `String intercept(String s)` can fit and more specific than `String intercept(Object o)`
        Assert.assertEquals(helloWorld, "String intercept(String s)");
    }

    // ============================================= ANNOTATION =============================================

    /*
     * @Argument
     * @AllArgument
     * @This
     * @Origin
     * @SuperCall
     * @Super
     * @RuntimeType
     * @DefaultCall
     * @Default
     * @Pipe
     *
     * extra:
     * @BindingPriority
     * @IgnoreForBinding
     * @Empty
     * @StubValue
     * @FieldValue
     * @FieldProxy
     * @Morph
     * @SuperMethod
     * @DefaultMethod
     */

    public static class Foo {

        public String name = "foo";

        public String foo(String s1, String s2) {
            return null;
        }
    }

    public static class TestArgument {
        public static String intercept(@Argument(1) String s1, @Argument(0) String s2) {
            return s1 + " " + s2;
        }
    }

    @Test
    public void testArgument() throws InstantiationException, IllegalAccessException {
        String foo = new ByteBuddy()
                .subclass(Foo.class)
                .method(named("foo"))
                .intercept(MethodDelegation.to(TestArgument.class))
                .make()
                .load(getClass().getClassLoader())
                .getLoaded()
                .newInstance()
                .foo("world", "hello");
        Assert.assertEquals(foo, "hello world");
    }


    public static class TestAllArguments {
        public static String intercept(@AllArguments String[] sa) {
            return Arrays.toString(sa);
        }
    }

    @Test
    public void testAllArgument() throws InstantiationException, IllegalAccessException {
        String foo = new ByteBuddy()
                .subclass(Foo.class)
                .method(named("foo"))
                .intercept(MethodDelegation.to(TestAllArguments.class))
                .make()
                .load(getClass().getClassLoader())
                .getLoaded()
                .newInstance()
                .foo("happiness", "health");
        Assert.assertEquals(foo, "[happiness, health]");
    }


    public static class TestThis {
        public static String intercept(@This Foo foo, @AllArguments String[] sa) {
            return foo.name;
        }
    }

    @Test
    public void testThis() throws InstantiationException, IllegalAccessException {
        String foo = new ByteBuddy()
                .subclass(Foo.class)
                .method(named("foo"))
                .intercept(MethodDelegation.to(TestThis.class))
                .make()
                .load(getClass().getClassLoader())
                .getLoaded()
                .newInstance()
                .foo("happiness", "health");
        Assert.assertEquals(foo, "foo");
    }


    public static class TestOrigin {
        public static String intercept(
                @Origin Class<?> clazz,
                @Origin Method method,/* @Origin Constructor<?> constructor,*/@Origin Executable executable,
                @Origin MethodHandle methodHandle, @Origin MethodType methodType,
                @Origin String methodString, @Origin int modifiers,
                @AllArguments String[] sa) {
            // debug here to see more
            System.out.println("clazz = " + clazz);
            System.out.println("method = " + method);
            System.out.println("executable = " + executable);
            System.out.println("methodHandle = " + methodHandle);
            System.out.println("methodType = " + methodType);
            System.out.println("methodString = " + methodString);
            System.out.println("modifiers = " + modifiers);
            return Arrays.toString(sa);
        }
    }

    @Test
    public void testOrigin() throws InstantiationException, IllegalAccessException {
        String foo = new ByteBuddy()
                .subclass(Foo.class)
                .method(named("foo"))
                .intercept(MethodDelegation.to(TestOrigin.class))
                .make()
                .load(getClass().getClassLoader())
                .getLoaded()
                .newInstance()
                .foo("happiness", "health");
        // 建议通过debug看变量详细信息
    }


    public static class MemoryDatabase {
        public int i = 0;

        public String load(String info) {
            return info + ": foo";
        }

        public final void setOne() {
            i = 1;
        }

        public void setTwo() {
            i = 2;
        }
    }

    public static class LoggerInterceptor {
        public static String log(@SuperCall Callable<String> zuper)
                throws Exception {
            // zuper is generated dynamically by bytebuddy
            // zuper has fields which represent origin method parameters
            System.out.println("Calling database");
            try {
                return zuper.call();
            } finally {
                System.out.println("Returned from database");
            }
        }
    }

    @Test
    public void testSuperCall() throws InstantiationException, IllegalAccessException {
        DynamicType.Loaded<MemoryDatabase> dynamicType = new ByteBuddy()
                .subclass(MemoryDatabase.class)
                .method(named("load"))
                .intercept(MethodDelegation.to(LoggerInterceptor.class))
                .make()
                .load(getClass().getClassLoader());

        String mysql = dynamicType.getLoaded()
                .newInstance().load("mysql");
        System.out.println(mysql); // watch output text

        // 辅助类型`AuxiliaryType`
        Class<?> superCallClass = dynamicType.getLoadedAuxiliaryTypes()
                .values().iterator().next();
        Class<?>[] interfaces = superCallClass.getInterfaces();
        Assert.assertTrue(Arrays.asList(interfaces).contains(Runnable.class));
        Assert.assertTrue(Arrays.asList(interfaces).contains(Callable.class));
    }

    public static class ChangingLoggerInterceptor {
        public static String log(String info, @Super MemoryDatabase zuper) {
            System.out.println("Calling database");
            try {
                zuper.setTwo(); // 委派给真正的实例执行，i=2
                zuper.i = 1;    // 操作的是辅助实例而不是真正的实例
                zuper.setOne();   // final方法无法委派给真正的实例
                return zuper.load(info + " (logged access)");
            } finally {
                System.out.println("Returned from database");
            }
        }
    }

    @Test
    public void testSuper() throws InstantiationException, IllegalAccessException {
        DynamicType.Loaded<MemoryDatabase> dynamicType = new ByteBuddy()
                .subclass(MemoryDatabase.class)
                .method(named("load"))
                .intercept(MethodDelegation.to(ChangingLoggerInterceptor.class))
                .make()
                .load(getClass().getClassLoader());

        MemoryDatabase memoryDatabase = dynamicType.getLoaded().newInstance();
        String mysql = memoryDatabase.load("mysql");
        System.out.println(mysql); // watch output text

        // 辅助类型`AuxiliaryType`
        Class<?> superCallClass = dynamicType.getLoadedAuxiliaryTypes()
                .values().iterator().next();
        Class<?> superclass = superCallClass.getSuperclass();
        Assert.assertSame(superclass, MemoryDatabase.class);

        Assert.assertSame(memoryDatabase.i, 2);
    }

    @SuppressWarnings("UnusedReturnValue")
    public static class Loop {
        public String loop(String value) {
            return value;
        }

        public int loop(int value) {
            return value;
        }
    }

    public static class Interceptor {
        @RuntimeType
        public static Object intercept(@RuntimeType Object value) {
            System.out.println("Invoked method with: " + value);
            return value;
        }
    }

    @Test
    public void testRuntimeType() throws InstantiationException, IllegalAccessException {
        Loop loop = new ByteBuddy()
                .subclass(Loop.class)
                .method(named("loop"))
                .intercept(MethodDelegation.to(Interceptor.class))
                .make()
                .load(getClass().getClassLoader())
                .getLoaded()
                .newInstance();

        // watch console
        loop.loop(123);
        loop.loop("abc");
    }

    public interface Flyable {
        default String fly() {
            return "i can fly";
        }

        void sayHi(String name);
    }

    public static class DefaultCallInterceptor {
        public static String intercept(@DefaultCall/*(targetType = Flyable.class)*/ Callable<String> callable) throws Exception {
            return "intercepted: " + callable.call();
        }
    }

    @Test
    public void testDefaultCall() throws InstantiationException, IllegalAccessException {
        Flyable flyable = (Flyable) new ByteBuddy()
                .subclass(Object.class)
                .implement(Flyable.class)
                .method(ElementMatchers.named("fly"))
                .intercept(MethodDelegation.to(DefaultCallInterceptor.class))
                .make()
                .load(getClass().getClassLoader())
                .getLoaded()
                .newInstance();
        String ret = flyable.fly();
        System.out.println(ret);
        Assert.assertEquals(ret, "intercepted: i can fly");
        try {
            flyable.sayHi("bob");
            Assert.fail();
        } catch (Throwable e) {
            System.out.println(e.getMessage());
            Assert.assertTrue(e instanceof AbstractMethodError);
        }

        @SuppressWarnings("DataFlowIssue")
        String path = getClass().getClassLoader().getResource("").getPath() + "testDefaultCall";
        System.out.println("path = " + path);
        try {
            new ByteBuddy()
                    .subclass(Flyable.class)
                    .method(ElementMatchers.named("fly"))
                    .intercept(MethodDelegation.to(DefaultCallInterceptor.class))
                    .make()
                    .saveIn(new File(path));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static class DefaultInterceptor {
        public static String intercept(@Default Flyable flyable) {
            System.out.println("proxy class: " + flyable.getClass());
            return "intercepted default: " + flyable.fly();
        }
    }

    @Test
    public void testDefault() throws InstantiationException, IllegalAccessException {
        Flyable flyable = new ByteBuddy()
                .subclass(Flyable.class)
                .method(named("fly"))
                .intercept(MethodDelegation.to(DefaultInterceptor.class))
                .make()
                .load(getClass().getClassLoader())
                .getLoaded()
                .newInstance();
        String ret = flyable.fly();
        Assert.assertEquals(ret, "intercepted default: i can fly");
    }

    public interface Forwarder<T, S> {
        T to(S target);
    }

    public static class ForwardingLoggerInterceptor {

        private final MemoryDatabase memoryDatabase; // constructor omitted

        public ForwardingLoggerInterceptor(MemoryDatabase memoryDatabase) {
            this.memoryDatabase = memoryDatabase;
        }

        public String log(@Pipe Forwarder<String, MemoryDatabase> pipe) {
            System.out.println("Calling database");
            try {
                return pipe.to(memoryDatabase);
            } finally {
                System.out.println("Returned from database");
            }
        }
    }

    public static class ForwardingLoggerInterceptor2 {

        private final MemoryDatabase memoryDatabase; // constructor omitted

        public ForwardingLoggerInterceptor2(MemoryDatabase memoryDatabase) {
            this.memoryDatabase = memoryDatabase;
        }

        public String log(@Pipe Function<MemoryDatabase, String> pipe) {
            System.out.println("Calling database");
            try {
                return pipe.apply(memoryDatabase);
            } finally {
                System.out.println("Returned from database");
            }
        }
    }

    @Test
    public void testPipe() throws Exception {
        MemoryDatabase loggingDatabase = new ByteBuddy()
                .subclass(MemoryDatabase.class)
                .method(ElementMatchers.named("load"))
                .intercept(MethodDelegation.withDefaultConfiguration()
                        .withBinders(Pipe.Binder.install(Forwarder.class))
                        .to(new ForwardingLoggerInterceptor(new MemoryDatabase())))
                .make()
                .load(getClass().getClassLoader())
                .getLoaded()
                .newInstance();
        loggingDatabase.load("hello world");

        MemoryDatabase loggingDatabase2 = new ByteBuddy()
                .subclass(MemoryDatabase.class)
                .method(ElementMatchers.named("load"))
                .intercept(MethodDelegation.withDefaultConfiguration()
                        .withBinders(Pipe.Binder.install(Function.class))
                        .to(new ForwardingLoggerInterceptor2(new MemoryDatabase())))
                .make()
                .load(getClass().getClassLoader())
                .getLoaded()
                .newInstance();
        loggingDatabase2.load("hello world");
    }

    // method delegation to instance

    public static class FoxInterceptor {
        public final String theField;

        public FoxInterceptor(String str) {
            theField = str;
        }

        @BindingPriority(10)
        public String instanceStr() {
            return theField;
        }

        @Override
        @BindingPriority(1)
        public String toString() {
            return "FoxInterceptor#toString()";
        }
    }

    public static class Fox {
        public final String instanceStr = "fox field";

        @Override
        public String toString() {
            return "fox";
        }
    }

    @Test
    public void testToInstance() throws Exception {
        String hello = "hello";
        Fox fox = new ByteBuddy()
                .subclass(Fox.class)
                .method(named("toString"))
                .intercept(MethodDelegation.to(new FoxInterceptor(hello)))
                .make()
                .load(getClass().getClassLoader())
                .getLoaded()
                .newInstance();
        String string = fox.toString();
        Assert.assertEquals(hello, string);
    }

    @Test
    public void testToField() throws Exception {
        Fox fox = new ByteBuddy()
                .subclass(Fox.class)
                .method(named("toString"))
                .intercept(MethodDelegation.toField("instanceStr"))
                .make()
                .load(getClass().getClassLoader())
                .getLoaded()
                .newInstance();
        Assert.assertEquals(fox.toString(), fox.instanceStr);
    }

    public static class Bar {
        public List<?> newList() {
            return new ArrayList<>();
        }
    }

    @Test
    public void testToConstructor() throws Exception {
        Bar bar = new ByteBuddy()
                .subclass(Bar.class)
                .method(named("newList"))
                .intercept(MethodDelegation.toConstructor(CopyOnWriteArrayList.class))
                .make()
                .load(getClass().getClassLoader())
                .getLoaded()
                .newInstance();
        List<?> list = bar.newList();
        Assert.assertTrue(list instanceof CopyOnWriteArrayList);
    }

}
